10.1 用户资料信息(个人基本资料)

1. 在app/models.py中增加用户信息字段:

1
2
3
4
5
6
7
8
9
# ...
class User(UserMixin, db.Model):
# ...
name = db.Column(db.String(64)) # 不同于username,可理解为昵称
location = db.Column(db.String(64))
about_me = db.Column(db.Text())
member_since = db.Column(db.DateTime(), default=datetime.utcnow()) # 注册时间
last_seen = db.Column(db.DateTime(), default=datetime.utcnow()) # 最后访问时间
  • db.String()db.Text()的区别是db.Text()不用指定最大长度。
  • db.Column()中的default参数可以接受函数名作为默认值,所以datetime.utcnow后面没有()

2. 在app/models.py中增加刷新用户最后访问时间功能:

1
2
3
4
5
6
7
8
# ...
class User(UserMixin, db.Model):
# ...
def ping(self):
self.last_seen = datenow.utcnow()
db.session.add(self)

3. 在app/auth/views.py中更新已登录用户的最后访问时间:

用户每次访问网站后,last_seen字段都需要进行更新,在每次请求前进行更新,可以满足这个需求。

1
2
3
4
5
6
7
8
9
10
# ...
@auth.before_app_request
def before_request():
if current_user.is_authenticated:
current_user.ping()
if not current_user.confirmd \
and request.endpoint[:5] != 'auth.' \
and request.endpoint != 'static':
return redirect(url_for('auth.unconfirmed'))

10.2 用户资料页面

1. 在app/main/views.py中定义用户资料页面路由:

1
2
3
4
5
6
7
8
9
from flask import abort
# ...
@main.route('/user/<username>')
def user(username):
user = User.query.filter_by(username=username).first()
if user is None:
abort(404)
return render_template('user.html', user=user)

2. 在app/templates/user.html编写用户资料页面模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{% extends "base.html" %}
{% block title %}Flasky - {{ user.username }}{% endblock %}
{% block page_content %}
<div class="page-header">
<img class="img-rounded profile-thumbnail" src="{{ user.gravatar(size=256) }}">
<div class="profile-header">
<h1>{{ user.username }}</h1>
{% if user.name or user.location %}
<p>
{% if user.name %}{{ user.name }}<br>{% endif %}
{% if user.location %}
from <a href="http://maps.google.com/?q={{ user.location }}">{{ user.location }}</a><br>
{% endif %}
</p>
{% endif %}
{% if current_user.is_administrator() %}
<p><a href="mailto:{{ user.email }}">{{ user.email }}</a></p>
{% endif %}
{% if user.about_me %}<p>{{ user.about_me }}</p>{% endif %}
<p>
Member since {{ moment(user.member_since).format('L') }}.
Last seen {{ moment(user.last_seen).fromNow() }}.
</p>
</div>
</div>
{% endblock %}

3. 在app/templates/base.html导航条中添加一个链接访问用户自己的资料页面:

1
2
3
4
5
6
7
{% if current_user.is_authenticated %}
<li>
<a href="{{ url_for('main.user', username=current_user.username) }}>
Profile
</a>
</li>
{% endif %}

10.3 用户资料编辑器

用户资料编辑分两种情况:(1)用户自己进入一个页面编辑自己的资料。(2)管理员能够编辑任意用户的资料,不仅包括用户的个人信息,而且还包括普通用户不能直接访问的User模型的字段,如用户角色等。

10.3.1 普通用户级别的资料编辑器

1. 在app/main/forms.py中添加普通用户用的资料编辑表单:

1
2
3
4
5
class EditProfileForm(FlaskForm):
name = StringField('Real name', validators=[Length(0, 64)])
location = StringField('Location', validators=[Length(0, 64)])
about_me = TextAreaField('About me')
submit = SubmitField('Submit')

注意:这个表单中所有字段都是可选的, 因此长度验证函数的长度允许为零

2. 在app/main/views.py中定义普通用户进行资料编辑的路由:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# ...
@main.route('/edit-profile', methods=['GET', 'POST'])
@login_required
def edit_profile():
form = EditProfileForm()
if form.validate_on_submit():
current_user.name = form.name.data
current_user.location = form.location.data
current_user.about_me = form.about_me.data
db.session.add(current_user._get_current_object())
db.session.commit()
flash('Your profile has been updated.')
return redirect(url_for('main.user', username=current_user.username))
# 在显示表单前(渲染表单前),为表单所有字段设定初始值
form.name.data = current_user.name
form.location.data = current_user.location
form.about_me.data = current_user.about_me
return render_template('edit_profile.html', form=form)

3. 在app/templates/user.html中添加普通用户用的编辑资料的链接:

1
2
3
4
5
6
7
# ...
{% if user == current_user %}
<a class="btn btn-default" href="{{ url_for('main.edit_profile') }}">
Edit Profile
</a>
{% endif %}

注意:链接外层的条件语句可以确保只有当用户查看自己的资料页面时才显示这个编辑个人资料的链接。

10.3.2 管理员级别的资料编辑器

1. 在app/main/forms.py中添加管理员用的资料编辑表单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# ...
class EditProfileAdminForm(FlaskForm):
email = StringField('Email', validators=[DataRequired(), Length(1, 64),
Email()])
username = StringField('Username', validators=[DataRequired(), Length(1, 64),
Regexp('^[A-Za-z][A-Za-z0-9_.]*$', 0,
'Username must be have only '
'letters, numbers, dots or'
'underscores')])
confirmed = BooleanField('Confirmed')
role = SelectField('Role', coerce=int)
name = StringField('Real name', validators=[Length(0, 64)])
location = StringField('Location', validators=[Length(0, 64)])
about_me = TextAreaField('About me')
submit = SubmitField('Submit')
def __init__(self, user, *args, **kwargs):
super(EditProfileAdminForm, self).__init__(*args, **kwargs)
self.role.choices = [(role.id, role.name)
for role in Role.query.order_by(Role.name).all()]
# 接受用户对象作为参数(成员变量)
self.user = user
def validate_email(self, field):
if field.data != self.user.email and \
User.query.filter_by(email=field.data).first():
raise ValidationError('Email already registered.')
def validate_username(self, field):
if field.data != self.user.username and \
User.query.filter_by(username=field.data).first():
raise ValidationError('Username already in use.')
  • SelectFiled()可以实现下拉列表,可用它来选择用户角色。
  • SelectFiled实例必须在其choices属性中设置可选选项,选项必须是由一个个元组组成的列表,各元组包含两个元素(选项的标识符和显示在控件中的文本字符串)。该例中,远足的标识符是角色的id,因为这个id是整数,所以添加coerce=int参数,从而把字段的值(字符串形式)转换为整数,从而保证这个字段的data属性值是整数。
  • emailusername都定义了自定义函数:验证这两个字段时,首先检查字段的值是否发生改变,如果发生改变,就要保证新值不和其他用户的相应字段重复;如果没有发生变化,则跳过该验证。

2. 在app/main/views.py中定义管理员进行资料编辑的路由:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
from app.decorators import admin_required
# ...
@main.route('/edit-profile/<int:id>', methods=['GET', 'POST'])
@login_required
@admin_required
def edit_profile_admin(id):
user = User.query.get_or_404(id)
form = EditProfileAdminForm(user=user)
if form.validate_on_submit():
user.email = form.email.data
user.username = form.username.data
user.confirmed = form.confirmed.data
user.role = Role.query.get(form.role.data)
user.name = form.name.data
user.location = form.location.data
user.about_me = form.about_me.data
db.session.add(user)
db.session.commit()
flash('The profile has been updated.')
return redirect(url_for('.user', username=user.username))
# 在显示表单前(渲染表单前),为表单所有字段设定初始值
form.email.data = user.email
form.username.data = user.username
form.confirmed.data = user.confirmed
# 将user.role_id赋值给表单字段
form.role.data = user.role_id
form.name.data = user.name
form.location.data = user.location
form.about_me.data = user.about_me
return render_template('edit_profile.html', form=form, user=user)
  • 该例中,要进行编辑的用户由id指定,因此可以使用Flask-SQLAlchemy提供的get_or_404()函数,如果提供的id不正确,则返回404错误。
  • user.role_id赋值给form.role.data是因为步骤1中choices属性设置的元组列表使用数字标识符表示各选项。
  • 提交表单后,角色idform.role.data中获取,并通过Role.query.get()方法加载角色对象。

3. 在app/templates/user.html中添加管理员用的资料编辑链接:

1
2
3
4
5
6
7
# ...
{% if current_user.is_administrator() %}
<a class="btn btn-danger" href="{{ url_for('main.edit_proflie_admin', id=user.id) }}>
Edit Profile [Admin]
</a>
{% endif %}

10.4 用户头像(使网站支持Gravatar提供的用户头像)

Gravatar可以把头像和电子邮件地址关联起来,使得你在支持Gravatar服务的网站中只需要头像URL就能显示你的头像(网站没有头像功能也能显示用户头像,只要网站支持Gravatar)。

头像URL由https://secure.gravatar.com/avatar/+电子邮件地址的MD5散列值组成。在浏览器地址栏输入该URL就会看到某电子邮件对应的头像(前提是到http://gravatar.com中注册账户并上传头像),如果没有对应的头像,则会显示一个默认图片。

头像URL的查询字符串可以包含多个参数,以此配置头像图片的特征(如像素大小等),可设参数如表10-1所示:
表10-1 Gravatar查询字符串参数

参数名 说明
s 图片大小,单位是像素
r 图片级别(有没有暴力倾向等)。可选值有'g''pg''r''x'
d 没有注册Gravatar服务的用户使用默认图片生成方式。可选值有:(1)'404',返回404错误;(2)默认图片的URL;(3)图片生成器:'mm''identicon''monsterid''wavatar''retro''blank'之一
fd 强制使用默认头像

1. 在app/models.py中增加avatar_hash字段(用于存储邮箱地址的MD5散列值)、生成邮箱地址的MD5三散列值方法、生成头像URL方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import hashlib
# ...
class User(UserMixin, db.Model):
# ...
avatar_hash = db.Column(db.String(32))
def __init__(self, **kwargs):
super(User, self).__init__(**kwargs)
if self.role is None:
if self.email == current_app.config['FLASKY_ADMIN']:
self.role = Role.query.filter_by(name='Administrator').first()
if self.role is None:
self.role = Role.query.filter_by(default=True).first()
if self.email is not None and self.avatar_hash is None:
self.avatar_hash = self.gravatar_hash()
# 定义生成邮箱地址MD5散列值的方法
def gravatar_hash(self):
return hashlib.md5(self.emial.lower().encode('utf-8')).hexdigest()
# 定义生成头像URL的方法
def gravatar(self, size=100, default='identicon', rating='g'):
url = 'https://secure.gravatar.com/avatar'
hash = self.gravatar_hash()
return '{url}/{hash}?s={size}&d={default}&r={rating}'.format(
url=url, hash=hash, size=size, default=default, rating=rating)

注意:由于生成MD5值是一项CPU密集型操作,所以要在某个页面中生成大量头像,计算量会很大。考虑到电子邮件地址的MD5散列值是不变的(电子邮件地址不变前提下),因此可以将MD5值存储到User模型字段中,而不必每次都进行计算,从而得到优化。

2. 在app/templates/user.html中增加资料页面中的头像显示:

1
2
3
# ...
<img class="img-rounded profile-thumbnail" src="{{ user.gravatar(size=256) }}">

3. 在`app/templates/base.html”中导航条上添加已登录用户的头像缩略图:

为了更好地调整页面中头像图片的显示格式,可自定义CSS类,将其放在statics文件夹中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# ...
{% block head %}
{{ super() }}
# ...
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='styles.css') }}">
{% endblock %}
{% blcok navbar %}
# ...
<ul class="nav navbar-nav navbar-right">
{% if current_user.is_authenticated %}
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<img src="{{ current_user.gravatar(size=18) }}">
Account <b class="caret"></b>
</a>
# ...
{% endblock %}